Add artifact relations tool and enrich fetch_artifacts with relation metadata#11
Add artifact relations tool and enrich fetch_artifacts with relation metadata#11
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new get_artifact_relations tool to the CodeAlive MCP server, enabling exploration of call graphs, inheritance hierarchies, and symbol references. Additionally, the fetch_artifacts tool has been updated to include call relations in its XML output and wrap source code within a new <content> element. Review feedback focuses on enhancing the robustness of XML generation by ensuring that potential null values from API responses are handled safely before calling html.escape and that all XML attributes are consistently escaped to prevent malformed output.
| source_id = html.escape(data.get("sourceIdentifier", "")) | ||
| profile = html.escape(data.get("profile", "")) | ||
| found = data.get("found", False) | ||
|
|
||
| # Map profile back to MCP-friendly name | ||
| mcp_profile = profile | ||
| for mcp_name, api_name in PROFILE_MAP.items(): | ||
| if api_name == profile: | ||
| mcp_profile = mcp_name | ||
| break | ||
|
|
||
| if not found: | ||
| return f'<artifact_relations sourceIdentifier="{source_id}" profile="{mcp_profile}" found="false"/>' |
There was a problem hiding this comment.
There are two issues in this block:
- Potential Crash:
html.escapewill raise anAttributeErrorif the API returnsnullforsourceIdentifierorprofile, becausedict.get(key, default)returnsNoneif the key is present with anullvalue. Usingdata.get("key") or ""ensures a string is always passed. - Logic Error: The profile mapping logic compares the escaped
profilestring with the rawapi_namefromPROFILE_MAP. If the profile name contains characters that require escaping (e.g.,&), the comparison will fail. Mapping should occur on the raw value before escaping for XML.
| source_id = html.escape(data.get("sourceIdentifier", "")) | |
| profile = html.escape(data.get("profile", "")) | |
| found = data.get("found", False) | |
| # Map profile back to MCP-friendly name | |
| mcp_profile = profile | |
| for mcp_name, api_name in PROFILE_MAP.items(): | |
| if api_name == profile: | |
| mcp_profile = mcp_name | |
| break | |
| if not found: | |
| return f'<artifact_relations sourceIdentifier="{source_id}" profile="{mcp_profile}" found="false"/>' | |
| raw_source_id = data.get("sourceIdentifier") or "" | |
| raw_profile = data.get("profile") or "" | |
| found = data.get("found", False) | |
| # Map profile back to MCP-friendly name | |
| mcp_profile = raw_profile | |
| for mcp_name, api_name in PROFILE_MAP.items(): | |
| if api_name == raw_profile: | |
| mcp_profile = mcp_name | |
| break | |
| source_id_attr = html.escape(raw_source_id) | |
| profile_attr = html.escape(mcp_profile) | |
| if not found: | |
| return f'<artifact_relations sourceIdentifier="{source_id_attr}" profile="{profile_attr}" found="false"/>' |
| attrs = [f'identifier="{html.escape(item.get("identifier", ""))}"'] | ||
|
|
||
| file_path = item.get("filePath") | ||
| if file_path is not None: | ||
| attrs.append(f'filePath="{html.escape(file_path)}"') | ||
|
|
||
| start_line = item.get("startLine") | ||
| if start_line is not None: | ||
| attrs.append(f'startLine="{start_line}"') | ||
|
|
There was a problem hiding this comment.
Similar to previous comments, html.escape will crash if identifier is null. Additionally, startLine should be escaped for XML robustness.
| attrs = [f'identifier="{html.escape(item.get("identifier", ""))}"'] | |
| file_path = item.get("filePath") | |
| if file_path is not None: | |
| attrs.append(f'filePath="{html.escape(file_path)}"') | |
| start_line = item.get("startLine") | |
| if start_line is not None: | |
| attrs.append(f'startLine="{start_line}"') | |
| for item in group.get("items", []): | |
| attrs = [f'identifier="{html.escape(item.get("identifier") or "")}"'] | |
| file_path = item.get("filePath") | |
| if file_path is not None: | |
| attrs.append(f'filePath="{html.escape(file_path)}"') | |
| start_line = item.get("startLine") | |
| if start_line is not None: | |
| attrs.append(f'startLine="{html.escape(str(start_line))}"') |
| call_id = html.escape(call.get("identifier", "")) | ||
| summary = call.get("summary") | ||
| if summary is not None: | ||
| call_elements.append( | ||
| f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>' | ||
| ) | ||
| else: | ||
| call_elements.append(f' <call identifier="{call_id}"/>') | ||
|
|
||
| parts.append(f' <{tag} count="{count}">') |
There was a problem hiding this comment.
Ensure that identifier is not null before passing it to html.escape to avoid an AttributeError. Also, escape the count attribute to ensure the generated XML is robust against unexpected API response types.
| call_id = html.escape(call.get("identifier", "")) | |
| summary = call.get("summary") | |
| if summary is not None: | |
| call_elements.append( | |
| f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>' | |
| ) | |
| else: | |
| call_elements.append(f' <call identifier="{call_id}"/>') | |
| parts.append(f' <{tag} count="{count}">') | |
| call_id = html.escape(call.get("identifier") or "") | |
| summary = call.get("summary") | |
| if summary is not None: | |
| call_elements.append( | |
| f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>' | |
| ) | |
| else: | |
| call_elements.append(f' <call identifier="{call_id}"/>') | |
| parts.append(f' <{tag} count="{html.escape(str(count))}">') |
| total_count = group.get("totalCount", 0) | ||
| returned_count = group.get("returnedCount", 0) | ||
| truncated = str(group.get("truncated", False)).lower() | ||
|
|
||
| xml_parts.append( | ||
| f' <relation_group type="{html.escape(mcp_type)}" ' | ||
| f'totalCount="{total_count}" returnedCount="{returned_count}" ' | ||
| f'truncated="{truncated}">' | ||
| ) |
There was a problem hiding this comment.
This block has robustness issues:
- If
totalCountorreturnedCountarenullin the JSON response,group.get(..., 0)will returnNone, which might cause issues later or result in the string"None"in the XML. - If
truncatedisnull,str(None).lower()results in"none", which is not a valid XML boolean value (true/false). - Attributes should be escaped to ensure the XML remains well-formed even if the API returns unexpected string values.
| total_count = group.get("totalCount", 0) | |
| returned_count = group.get("returnedCount", 0) | |
| truncated = str(group.get("truncated", False)).lower() | |
| xml_parts.append( | |
| f' <relation_group type="{html.escape(mcp_type)}" ' | |
| f'totalCount="{total_count}" returnedCount="{returned_count}" ' | |
| f'truncated="{truncated}">' | |
| ) | |
| total_count = group.get("totalCount") or 0 | |
| returned_count = group.get("returnedCount") or 0 | |
| truncated = str(bool(group.get("truncated"))).lower() | |
| xml_parts.append( | |
| f' <relation_group type="{html.escape(mcp_type)}" ' | |
| f'totalCount="{html.escape(str(total_count))}" returnedCount="{html.escape(str(returned_count))}" ' | |
| f'truncated="{truncated}">' | |
| ) |
Adds a new
get_artifact_relationsMCP tool and enrichesfetch_artifactswith optional call relation metadata.